# -*- coding: utf-8 -*-

from genetic_algorithm import genetic_algorithm
from tools import output_utls, input_utls , nfp_utls
from settings import SPACING, POPULATION_SIZE, MUTA_RATE, CURVETOLERANCE, BIN_LENGTH, BIN_WIDTH
import json
from tools.nfp_utls import almost_equal, rotate_polygon, get_polygon_bounds, polygon_area,nfp_rectangle
import pyclipper
import copy
import numpy as np

class Nester:
    def __init__(self, container=None, shapes=None, generation_num=1):
        self.container = container
        self.shapes = shapes
        self.total_segments_area = 0
        self.results = list()                           # storage for the different results
        self.nfp_cache = {}
        
        self.rotations = [0,90]
        self.populationSize = 10
        self.mutationRate =0.2
        self.spacing = SPACING

        self.GA = None
        self.generation = generation_num
        self.best = None
        self.solution = None

    def set_segments(self, segments_lists):
        shapes_lst = []
        total_area = 0
        offset = self.spacing
        for segment_cord in segments_lists:
            p_id = segment_cord['p_id']
            texture = segment_cord['texture']
            length = segment_cord['length'] + offset
            width = segment_cord['width'] + offset

            area = length * width
            segment_shape = [[0, 0], [length, 0], [length, width], [0, width]]
            item = [{'x': p[0], 'y': p[1]} for p in segment_shape]
            shape = {'p_id': p_id,
                     'texture': texture,
                     'length': length,
                     'width': width,
                     'area': area,
                     'points': item
                     }
            total_area = total_area + area
            shapes_lst.append(shape)

        self.total_segments_area = total_area
        self.shapes_lst = shapes_lst

    def set_container(self, bin_length, bin_width ):
        container = {}
        container_cord = [[0, 0], [bin_length, 0], [bin_length, bin_width], [0, bin_width]]
        container['p_id'] = '-1'
        container['length'] = bin_length
        container['width'] = bin_width
        container['area'] = bin_length * bin_width
        container['points'] = [{'x': p[0], 'y': p[1]} for p in container_cord]
        self.container = container

    def set_patches(self):
        pass

    def run(self):

        segments_sorted_list01 = sorted(self.shapes_lst, reverse=True, key=lambda o_segment:  max(o_segment['length'],o_segment['width']))
        segments_sorted_list = sorted(segments_sorted_list01, reverse=True, key=lambda o_segment: o_segment['area'])
        ####################################################################################
        generation = 0
        while generation < self.generation:
            if self.GA is None:
                container = self.container
                self.GA = genetic_algorithm.genetic_algorithm(segments_sorted_list, container)
            else:
                # for i in range(0, self.GA.populationSize):
                #     result_info_dic = self.find_fitness(self.GA.population[i])
                #     self.GA.population[i]['fitness'] = result_info_dic['fitness']
                #     self.results.append(result_info_dic)
                # if len(self.results) > 0:
                #     best_result = self.results[0]
                #     for p in self.results:
                #         if p['fitness'] > best_result['fitness']:
                #             best_result = p
                #     if self.best is None or best_result['fitness'] > self.best['fitness']:
                #         self.best = best_result
                # self.GA.generation()

                solution = self.GA.population[0]

                fitness_lst = []
                count_ind = 0
                while len(solution) > 0:
                    print('shapes length : ', len(solution))
                    nfp_pairs = self.generate_nfp_paris(solution)

                    segment_cache = {}
                    for pairs in nfp_pairs:
                        key = json.dumps(pairs['key'])
                        segment_cache[key] = {'A': pairs['A'], 'B': pairs['B']}

                    nfp_cache = self.generate_nfp(nfp_pairs)

                    #### 开始 排版
                    result = self.place_paths( solution, nfp_cache, segment_cache)

                    placed_segment_list = result['placed_segment_list']
                    pure_fitness = result['pure_fitness']

                    result_file_name = "L000" + str(count_ind + 1) + '_' + str(len(placed_segment_list)) + '_' + str(round(pure_fitness, 3))
                    rotated_shapes = [p['points'] for p in placed_segment_list]
                    output_utls.output_result(result['placements'], rotated_shapes, container, result['pure_fitness'], result_file_name)

                    pids_lst = [x['p_id'] for x in result['placements']]
                    solution = [s for s in solution if s['p_id'] not in pids_lst]

                    fitness_lst.append(pure_fitness)
                    count_ind = count_ind + 1

                print('full_fitness: ', sum(fitness_lst[:-1]) / (count_ind - 1))
                print('consume of panel: ', count_ind )
                print( 'total_fitness: ', self.total_segments_area / (1220 * 2440 * count_ind))

                generation = generation + 1
                print("generation = ", generation)

    ####################################################################################
    def place_paths(self, solution, nfp_cache, segment_cache):
        """
        输入1： 各个零件的piont,
        输入2： 每个零件(包含面板)之间的nfp
        :param solution: ##
        :param nfp_cache: ##
        输出：
        :return: ##
        """
        placed_segment_list = []
        movements_list = []

        for segment in solution:
            texture_seg = segment['texture']
            if len(placed_segment_list) == 0:
                movement = None
                height_base = self.container['width']
                height_inst = segment['width']
                length_inst = segment['length']
                if ( height_inst > height_base or (height_inst < length_inst <= height_base) ) and texture_seg < 1:
                    key = json.dumps({'A': '-1', 'B': segment['p_id'], 'A_rotation': 0, 'B_rotation': 90})
                    segment01 = segment_cache[key]['B']
                    inner_fit_rectangle = nfp_cache.get(key)  ### 获取零件放在面料板，内侧的nfp
                    for point in inner_fit_rectangle:
                        if movement is None or (point['x'] - segment01['x'] < movement['x']):  ### 找到nfp最左侧的候选点
                            movement = {
                                'x': point['x'] - segment01['x'],
                                'y': point['y'] - segment01['y'],
                                'p_id': segment['p_id'],
                                'rotation': 90}
                else:
                    key = json.dumps({'A': '-1', 'B': segment['p_id'], 'A_rotation': 0, 'B_rotation': 0})
                    segment01 = segment_cache[key]['B']
                    inner_fit_rectangle = nfp_cache.get(key)  ### 获取零件放在面料板，内侧的nfp
                    for point in inner_fit_rectangle:
                        if movement is None or (point['x'] - segment01['x'] < movement['x']):  ### 找到nfp最左侧的候选点
                            movement = {
                                'x': point['x'] - segment01['x'],
                                'y': point['y'] - segment01['y'],
                                'p_id': segment['p_id'],
                                'rotation': 0}
                movements_list.append(movement)
                placed_segment_list.append(segment01)
                continue

            contrast_min_area = {0: None, 90: None}
            contrast_movement = {0: None, 90: None}
            contrast_segment01 = {0: None, 90: None}

            if texture_seg < 1:
                rotations_lst =  [0, 90]
            else:
                rotations_lst =  [0]

            for b_rota in rotations_lst:
                key = json.dumps({'A': '-1', 'B': segment['p_id'], 'A_rotation': 0, 'B_rotation': b_rota})
                segment01 = segment_cache[key]['B']
                inner_fit_rectangle = nfp_cache.get(key)
                if inner_fit_rectangle is None:
                    continue
                clipper_bin_nfp = list()
                clipper_bin_nfp.append([[p['x'], p['y']] for p in inner_fit_rectangle])
                clipper = pyclipper.Pyclipper()
                j = 0
                for placed_segment in placed_segment_list:
                    key = json.dumps(
                        {'A': placed_segment['p_id'], 'B': segment['p_id'], 'A_rotation': placed_segment['rotation'],
                         'B_rotation': b_rota})
                    nfp = nfp_cache.get(key)
                    moved_nfp_bl = [[point['x'] + movements_list[j]['x'], point['y'] + movements_list[j]['y']] for point
                                    in nfp]  ## nfp move
                    moved_nfp_bl = pyclipper.CleanPolygon(moved_nfp_bl)  ### 移除moved_nfp_bl上无关的点集
                    j = j + 1
                    if len(moved_nfp_bl) > 2:
                        clipper.AddPath(moved_nfp_bl, pyclipper.PT_SUBJECT, True)  ### moved_nfp_bl加入主题
                combine_nfp = clipper.Execute(pyclipper.CT_UNION, pyclipper.PFT_NONZERO, pyclipper.PFT_NONZERO)
                clipper = pyclipper.Pyclipper()
                clipper.AddPaths(combine_nfp, pyclipper.PT_CLIP, True)
                clipper.AddPaths(clipper_bin_nfp, pyclipper.PT_SUBJECT, True)  ## segment的内侧nfp 加入主题
                final_nfp = clipper.Execute(pyclipper.CT_DIFFERENCE, pyclipper.PFT_NONZERO, pyclipper.PFT_NONZERO)
                final_nfp = pyclipper.CleanPolygons(final_nfp)
                ### final_nfp 是 求待放的零件segment与已放了的零件placed_segment的moved_nfp_bl  和 求待放的零件segment的内侧clipper_bin_nfp 的组合有效final_nfp
                for j in range(len(final_nfp) - 1, -1, -1):
                    if len(final_nfp[j]) < 3:  ## 检查是否有nfp 小于三
                        final_nfp.pop(j)
                if len(final_nfp) == 0:
                    continue
                candidate_bl_position = list()
                for polygon in final_nfp:
                    for p in polygon:
                        ### 将所有可能放置的点都存放在一个list里面，这些点就是candidate position
                        candidate_bl_position.append({'x': p[0], 'y': p[1]})

                movement = None
                min_weight_xy = None
                ### final_nfp 就是候选点
                for p_nf in candidate_bl_position:
                    all_points = list()
                    for m in range(0, len(placed_segment_list)):  ### 已存放的零件进行move
                        for p in placed_segment_list[m]['points']:
                            all_points.append({
                                'x': p['x'] + movements_list[m]['x'],
                                'y': p['y'] + movements_list[m]['y']})
                    # path 坐标
                    shift_vector = {
                        'x': p_nf['x'] - segment['points'][0]['x'],  ### 候选的piont 进行move
                        'y': p_nf['y'] - segment['points'][0]['y'],
                        'p_id': segment['p_id'],
                        'rotation': b_rota,
                    }
                    new_pionts = []
                    # 找新坐标后的最小矩形
                    for point in segment['points']:
                        all_points.append({
                            'x': point['x'] + shift_vector['x'],
                            'y': point['y'] + shift_vector['y']
                        })
                        new_pionts.append([point['x'] + shift_vector['x'], point['y'] + shift_vector['y']])

                    new_x01 = np.mean([x[0] for x in new_pionts])
                    new_y01 = np.mean([x[1] for x in new_pionts])
                    weight_xy = min(2440 - new_x01, new_x01) + min(1220 - new_y01, new_y01)
                    ##weight_old = min( [ 4*pow(x[0]-1220,2)+pow(x[1]-610,2) for x in old_pionts ] )
                    ##weight_xy = min( [ 4*pow(x[0]-1220,2)+pow(x[1]-610,2) for x in new_pionts ] )
                    rect_bounds = get_polygon_bounds(all_points)
                    rect_length = rect_bounds['length']
                    rect_width = rect_bounds['width']
                    area = rect_length * rect_width
                    if area <= 1220 * 2440 and rect_length <= 2440 and rect_width <= 1220:
                        if (min_weight_xy is None or weight_xy < min_weight_xy or almost_equal(min_weight_xy, weight_xy)):
                            min_weight_xy = weight_xy
                            movement = shift_vector

                contrast_min_area[b_rota] = min_weight_xy
                contrast_movement[b_rota] = movement
                contrast_segment01[b_rota] = segment01
            if contrast_min_area[0] is None and contrast_min_area[90] is None:
                continue
            elif contrast_min_area[0] is not None and contrast_min_area[90] is not None:
                if contrast_min_area[0] <= contrast_min_area[90]:
                    if contrast_movement[0]:
                        movements_list.append(contrast_movement[0])
                        placed_segment_list.append(contrast_segment01[0])
                else:
                    if contrast_movement[90]:
                        movements_list.append(contrast_movement[90])
                        placed_segment_list.append(contrast_segment01[90])
            elif contrast_min_area[0] is None and contrast_min_area[90] is not None:
                if contrast_movement[90]:
                    movements_list.append(contrast_movement[90])
                    placed_segment_list.append(contrast_segment01[90])
            elif contrast_min_area[0] is not None and contrast_min_area[90] is None:
                if contrast_movement[0]:
                    movements_list.append(contrast_movement[0])
                    placed_segment_list.append(contrast_segment01[0])

        min_area_sum = sum([p['length'] * p['width'] for p in placed_segment_list])
        pure_fitness = min_area_sum / (1220 * 2440)

        result = {'placements': movements_list,
                  'placed_segment_list': placed_segment_list,
                  'pure_fitness': pure_fitness,
                  'solution': solution}
        assert result['pure_fitness'] <= 1.0
        return result

    def generate_nfp_paris(self, solution ):
        nfp_pairs = []
        for i in range(len(solution)):
            segment_i = solution[i]
            texture_i = segment_i['texture']
            segment_i_rota90 = rotate_polygon(segment_i, 90)
            segment_i_rota00 = rotate_polygon(segment_i, 0)
            container00 = rotate_polygon(self.container, 0)
            #############
            key = {'A': '-1', 'B': segment_i['p_id'], 'A_rotation': 0, 'B_rotation': 0}
            nfp_pairs.append({'A': container00, 'B': segment_i_rota00, 'key': key})
            #############
            if texture_i < 1 :
                key = {'A': '-1', 'B': segment_i['p_id'], 'A_rotation': 0, 'B_rotation': 90}
                nfp_pairs.append({'A': container00, 'B': segment_i_rota90, 'key': key})
            ##############################################################################
            for j in range(0, i):
                placed_segment_j = solution[j]
                texture_j = placed_segment_j['texture']
                placed_segment_j_rota90 = rotate_polygon(placed_segment_j, 90)
                placed_segment_j_rota00 = rotate_polygon(placed_segment_j, 0)
                #### segment_i 的 NFP
                key = {'A': placed_segment_j['p_id'], 'B': segment_i['p_id'], 'A_rotation': 0, 'B_rotation': 0}
                nfp_pairs.append({'A': placed_segment_j_rota00, 'B': segment_i_rota00, 'key': key})
                #############
                if texture_j < 1 :
                    key = {'A': placed_segment_j['p_id'], 'B': segment_i['p_id'], 'A_rotation': 90, 'B_rotation': 0}
                    nfp_pairs.append({'A': placed_segment_j_rota90, 'B': segment_i_rota00, 'key': key})
                #############
                if texture_i < 1 :
                    key = {'A': placed_segment_j['p_id'], 'B': segment_i['p_id'], 'A_rotation': 0, 'B_rotation': 90}
                    nfp_pairs.append({'A': placed_segment_j_rota00, 'B': segment_i_rota90, 'key': key})
                    #############
                    if texture_j < 1 :
                        key = {'A': placed_segment_j['p_id'], 'B': segment_i['p_id'], 'A_rotation': 90, 'B_rotation': 90}
                        nfp_pairs.append({'A': placed_segment_j_rota90, 'B': segment_i_rota90, 'key': key})
        return nfp_pairs

    def generate_nfp(self, nfp_pairs):
        """
        :param nfp_paris:
        :return:
        """
        nfp_list = []
        for pair in nfp_pairs:
            poly_a = pair['A']
            poly_b = pair['B']
            pair_a_id = pair['key']['A']
            if pair_a_id == '-1':
                nfp = nfp_rectangle(poly_a['points'], poly_b['points'])
                if nfp is not None and polygon_area(nfp) > 0:
                    nfp.reverse()
            else:
                nfp = minkowski_difference(poly_a, poly_b)  # 使用 Minkowski_difference和求两个零件的nfp, 考虑用lib
                if polygon_area(nfp) > 0:
                    nfp.reverse()
            nfp_list.append({'key': pair['key'], 'value': nfp})

        nfp_cache = {}
        for nfp in nfp_list:
            key = json.dumps(nfp['key'])
            nfp_cache[key] = nfp['value']

        return nfp_cache


def polygon_offset(polygon, offset, CURVETOLERANCE):
    """
    :param polygon: [{'x': , 'y': }...{'x': 'y': }]
    :param offset: 5
    :return:
    """
    polygon = [[p['x'], p['y']] for p in polygon]

    miter_limit = 2
    co = pyclipper.PyclipperOffset(miter_limit, CURVETOLERANCE)
    co.AddPath(polygon, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
    result = co.Execute(1*offset)

    result = [{'x': p[0], 'y':p[1]} for p in result[0]]
    return result


def minkowski_difference(A, B):
    """
    两个多边形的相切空间
    http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Functions/MinkowskiDiff.htm
    :param A:
    :param B:
    :return:
    """
    Ac = [[p['x'], p['y']] for p in A['points']]
    Bc = [[p['x'] * -1, p['y'] * -1] for p in B['points']]
    solution = pyclipper.MinkowskiSum(Ac, Bc, True)
    largest_area = None
    clipper_nfp = None
    for p in solution:
        p = [{'x': i[0], 'y':i[1]} for i in p]
        sarea = polygon_area(p)
        if largest_area is None or largest_area > sarea:
            clipper_nfp = p
            largest_area = sarea

    clipper_nfp = [{
                    'x': clipper_nfp[i]['x'] + Bc[0][0] * -1,
                    'y':clipper_nfp[i]['y'] + Bc[0][1] * -1
                   } for i in range(0, len(clipper_nfp))]
    return clipper_nfp


def minkow(A, B):
    Ac = [[p['x'], p['y']] for p in A]
    Bc = [[p['x'] * -1, p['y'] * -1] for p in B]

    solution = pyclipper.MinkowskiSum(Ac, Bc, True)
    largest_area = None
    clipper_nfp = None

    for p in solution:
        p = [{'x': i[0], 'y': i[1]} for i in p]
        sarea = nfp_utls.polygon_area(p)
        if largest_area is None or largest_area > sarea:
            clipper_nfp = p
            largest_area = sarea

    clipper_nfp = [{
        'x': clipper_nfp[i]['x'] + Bc[0][0] * -1,
        'y': clipper_nfp[i]['y'] + Bc[0][1] * -1
    } for i in range(0, len(clipper_nfp))]
    return clipper_nfp

